/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.ponly.uno4j.util;

import com.sun.star.beans.PropertyValue;
import com.sun.star.lang.XComponent;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.lib.connections.pipe.PipeConnection;
import com.sun.star.lib.uno.helper.UnoUrl;
import com.sun.star.lib.util.UrlToFileMapper;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;
import com.sun.star.util.CloseVetoException;
import com.sun.star.util.XCloseable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FilenameFilter;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.ProtectionDomain;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

/**
 * Libreoffice/Openoffice 工具类
 *
 * @author vacoor
 */
public abstract class UnoUtils {
    public static final String OS = System.getProperty("os.name", "");
    public static final boolean WINDOWS = OS.startsWith("Windows");
    public static final boolean MAC = OS.startsWith("Mac") || OS.startsWith("Darwin");
    public static final boolean JDK_64BIT = "64".equals(System.getProperty("sun.arch.data.model", System.getProperty("com.ibm.vm.bitmode")));

    private static final Logger LOG = LoggerFactory.getLogger(UnoUtils.class);
    private static boolean JPIPE_LOADED = false;
    public static final String JURT_VERSION;

    public static String getUniqueKey() {
        return Long.toHexString(System.currentTimeMillis());
    }

    /**
     * 根据给定的键值构建 PropertyValue
     *
     * @param key   键
     * @param value 值
     * @return PropertyValue 实例
     */
    public static PropertyValue of(String key, Object value) {
        PropertyValue pv = new PropertyValue();
        pv.Name = key;
        pv.Value = value;
        return pv;
    }

    /**
     * 将给定的多个 PropertyValue 转换为数组
     *
     * @param pvs PropertyValue 集合
     * @return PropertyValue 数组
     */
    public static PropertyValue[] of(PropertyValue... pvs) {
        return pvs;
    }

    /**
     * 给定 K-V 对创建对应的 PropertyValue 数组
     *
     * @param k1 键 K1
     * @param v1 键 K1 对应的值
     * @param k2 键 K2
     * @param v2 键 K2 对应的值
     * @return PropertyValue 数组
     */
    public static PropertyValue[] of(String k1, Object v1, String k2, Object v2) {
        return of(of(k1, v1), of(k2, v2));
    }

    /**
     * 给定 K-V 对创建对应的 PropertyValue 数组
     *
     * @param k1 键 K1
     * @param v1 键 K1 对应的值
     * @param k2 键 K2
     * @param v2 键 K2 对应的值
     * @param k3 键 K3
     * @param v3 键 K3 对应的值
     * @return PropertyValue 数组
     */
    public static PropertyValue[] of(String k1, Object v1, String k2, Object v2, String k3, Object v3) {
        return of(of(k1, v1), of(k2, v2), of(k3, v3));
    }

    /**
     * 给定 K-V 对创建对应的 PropertyValue 数组
     *
     * @param k1 键 K1
     * @param v1 键 K1 对应的值
     * @param k2 键 K2
     * @param v2 键 K2 对应的值
     * @param k3 键 K3
     * @param v3 键 K3 对应的值
     * @param k4 键 K4
     * @param v4 键 K4 对应的值
     * @return PropertyValue 数组
     */
    public static PropertyValue[] of(String k1, Object v1, String k2, Object v2, String k3, Object v3, String k4, Object v4) {
        return of(of(k1, v1), of(k2, v2), of(k3, v3), of(k4, v4));
    }

    /**
     * 合并给定的 PropertyValue 为新数组
     *
     * @param pvs1 PropertyValue 数组
     * @param pvs2 PropertyValue
     * @return 合并后的 PropertyValue 数组
     */
    public static PropertyValue[] merge(PropertyValue[] pvs1, PropertyValue... pvs2) {
        PropertyValue[] pvs = new PropertyValue[pvs1.length + pvs2.length];
        System.arraycopy(pvs1, 0, pvs, 0, pvs1.length);
        System.arraycopy(pvs2, 0, pvs, pvs1.length, pvs2.length);
        return pvs;
    }

    /**
     * 当 {@link com.sun.star.uno.UnoRuntime#queryInterface} 可以将 xComp 转换为 XCloseable
     * 并且成功调用 disconnect 时返回 true, 其余情况返回false
     *
     * @param xComp Uno 对象
     * @return 是否成功关闭
     */
    public static boolean closeIf(Object xComp) {
        XCloseable xCloseable = UnoRuntime.queryInterface(XCloseable.class, xComp);
        if (xCloseable != null) {
            try {
                xCloseable.close(true);
                return true;
            } catch (CloseVetoException ignore) {
                // ignore
            }
        }
        return false;
    }

    /**
     * 当 {@link com.sun.star.uno.UnoRuntime#queryInterface} 可以将 xComp 转换为 XComponent
     * 并且成功调用 dispose 时返回 true, 其余情况返回false
     *
     * @param xComp Uno 组件
     * @return 是否成功销毁
     */
    public static boolean disposeIf(Object xComp) {
        XComponent xComponent = $q(XComponent.class, xComp);
        if (xComponent != null) {
            xComponent.dispose();
            return true;
        }
        return false;
    }

    /**
     * 使用给定上下文和名称创建给定类型的组件
     *
     * @param ctx     上下文
     * @param type    目标组件类型
     * @param cmpName 组件名称
     * @param <T>     组件类型
     * @return 创建的组件
     * @throws com.sun.star.uno.Exception
     */
    public static <T> T $c(XComponentContext ctx, Class<T> type, String cmpName) throws Exception {
        return $q(type, $c(ctx, cmpName));
    }

    /**
     * 使用给定的 XComponentContext 创建给定名称的组件
     *
     * @param ctx     上下文
     * @param cmpName 组件名称
     * @return 组件实例
     * @throws com.sun.star.uno.Exception
     */
    public static Object $c(XComponentContext ctx, String cmpName) throws Exception {
        XMultiComponentFactory xMultiCmpFactory = ctx.getServiceManager();
        return xMultiCmpFactory.createInstanceWithContext(cmpName, ctx);
    }

    /**
     * 获取给定 Uno 对象的 type 类型接口 (使用 UnoRuntime.queryInterface)
     *
     * @param type 接口类型
     * @param obj  Uno 对象
     * @param <T>  接口类型
     * @return 给定 Uno 对象的接口或null
     */
    public static <T> T $q(Class<T> type, Object obj) {
        return (T) UnoRuntime.queryInterface(type, obj);
    }

    /**
     * 获取 Libreoffice/Openoffice 的 soffice 可执行文件路径
     * <p/>
     * 5.x 之前:
     * windows: $OO_HOME/program/soffice.exe
     * mac osx: $OO_HOME/MacOS/soffice
     * linux:   $OO_HOME/program/soffice.bin
     * <p/>
     * 5.x:
     * windows: $OO_HOME/program/soffice.exe
     * mac osx: $OO_HOME/MacOS/soffice
     * linux:   $OO_HOME/program/soffice
     *
     * @param ooHome OO_HOME
     * @return
     */
    public static String getSoffice(String ooHome) {
        if (ooHome == null) {
            return null;
        }

        // unix for default (< 5.x)
        String relative = "program/soffice.bin";

        if (WINDOWS) {
            relative = "program/soffice.exe";
        } else if (MAC) {
            relative = "MacOS/soffice";
        } else if (!new File(ooHome, relative).exists()) {
            // 可能是 unix libreoffice 5.x
            relative = "program/soffice";
        }
        return new File(ooHome, relative).getAbsolutePath();
    }

    /**
     * find OO_HOME from generic install directory
     * <p/>
     * mac os x: "/Applications" and "$HOME/Applications", ( OO_HOME is OO_APP/Contents )
     * linux:   "/opt" and "/usr/lib"
     * windows: "%ProgramFiles%" and "%ProgramFiles(x86)%"
     *
     * @return
     */
    public static String findOOHome() {
        // java system property
        String ooHome = System.getProperty("oo.home");

        // env
        if (ooHome == null) {
            ooHome = System.getenv("OO_HOME");
        }

        if (ooHome != null) {
            return ooHome;
        }

        // try find
        if (WINDOWS) {
            return findOODirectory(System.getenv("ProgramFiles"), System.getenv("ProgramFiles(x86)"));
        }

        if (MAC) {
            ooHome = findOODirectory("/Applications", System.getProperty("user.home") + "/Applications");
            return ooHome != null ? ooHome + "/Contents" : null;
        }

        // unix
        return findOODirectory("/opt", "/usr/lib", "/usr/lib64");
    }

    /**
     * find starts width "openoffice" or "libreoffice" dir in given directories
     *
     * @param searchDirs search dirs
     * @return starts with "openoffice" or "libreoffice" dir
     */
    private static String findOODirectory(String... searchDirs) {
        for (String d : searchDirs) {
            if (d == null || d.length() == 0) {
                continue;
            }
            File dir = new File(d);
            if (!dir.isDirectory()) {
                continue;
            }

            File[] files = dir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    name = name.toLowerCase();
                    return name.startsWith("libreoffice") || name.startsWith("openoffice");
                }
            });

            if (files != null && files.length > 0) {
                return files[0].getAbsolutePath();
            }
        }

        return null;
    }

    /**
     * 如果需要 加载 JPIPE 本地库 (pipe 连接类型需要)
     * <p/>
     * 如果要加载 pipe, 相关依赖请使用 openoffice 3.2.1 maven 依赖, 例如:<br>
     * <code>
     * <pre>
     * <dependency>
     *    <groupId>org.openoffice</groupId>
     *    <artifactId>unoil</artifactId>
     *    <version>3.2.1</version>
     * </dependency>
     * </pre>
     * </code>
     * 4.x, 5.x 版本把原来的 catch 去掉了, com.sun.star.lib.connections.pipe.PipeConnection 中 使用静态代码块来加载 native library,
     * 要求 jpipe 必须在 classpath, 但是 jpipe 有依赖众多东西, 没法搞呀
     * <code>
     * <pre>
     * static {
     *     NativeLibraryLoader.loadLibrary(PipeConnection.class.getClassLoader(), "jpipe");
     * }
     * </pre>
     * </code>
     *
     * @param ooHome OO_HOME (libreoffice/openoffice home), 如果为 null, 则自动查找
     */
    public static void loadJPipeLibraryIfNecessary(String ooHome) {
        // 大于等于 4.x
        if (1 > "4".compareToIgnoreCase(JURT_VERSION)) {
            LOG.warn("OOo SDK jurt.jar version is {}, pipe protocol may not be able to work, org.openoffice:jurt:3.x is recommended for pipe.", JURT_VERSION);
        }
        // 已经加载或连接类型不是 pipe
        if (JPIPE_LOADED) {
            return;
        }

        /*-
         * sal native library 在加载 jpipe 时会自动加载
         * sal name:
         *     "libuno_sal.dylib.3" in mac osx,
         *     "sal.dll" in windows
         */
        final String library = "jpipe";
        synchronized (UnoUtils.class) {
            if (JPIPE_LOADED) {
                return;
            }

            try {
                // 1. 首先, 尝试从first, try load from java.library.path
                System.loadLibrary(library);

                LOG.debug("load native library '{}' from java.library.path", library);

                JPIPE_LOADED = true;
                return;
            } catch (UnsatisfiedLinkError ignore) {
                // ignore
                LOG.debug("Can't load native library '{}', from java.library.path", library, ignore);
            }

            // TODO jvm 只在第一次加载 nativeLib 时根据 java.library.path 初始化一次查找路径, 这里重新查找一次
            /*-
              系统缓存了java.library.path的值，并且一直都会是第一次加载时的值。
              原因和ClassLoader的实现有关系，
              ClassLoader.loadLibrary() method:
              Java代码
              if (sys_paths == null) {
                 usr_paths = initializePath("java.library.path");
                 sys_paths = initializePath("sun.boot.library.path");
              }
             */
            String libraryPath = ooHome != null ? ooHome : findOOHome();
            if (null != libraryPath) {
                libraryPath = libraryPath.endsWith("/") ? libraryPath : libraryPath + "/";
                // libraryPath += (WINDOWS ? "URE/bin" : "ure-link/lib");
                /*-
                 * windows: OO_HOME/URE/program/jpipe.dll
                 * mac: OO_HOME/Frameworks(OO_HOME/MacOS/urelibs链接, 注: MAC下 OO_HOME为 o.app/Contents/)
                 * unix: OO_HOME/ure-link/lib
                 */
                libraryPath += (WINDOWS ? "program" : (MAC ? "Frameworks" : "ure-link/lib"));
            }

            String path = null;
            try {
                // 2. 其次, 尝试使用绝对路径加载
                path = new File(libraryPath, library).getAbsolutePath();
                System.load(path);

                LOG.debug("load native library  {}", path);

                JPIPE_LOADED = true;
                return;
            } catch (UnsatisfiedLinkError ignore) {
                // ignore
                LOG.debug("Can't load native library  {}", path, ignore);
            }

            try {
                // 3. 最后, 尝试使用映射名称的绝对路径进行加载
                // mac mapLibraryName("jpipe") --> libjpipe.dylib, real libjpipe.jnilib
                path = new File(libraryPath, System.mapLibraryName(library).replaceAll("dylib$", "jnilib")).getAbsolutePath();
                System.load(path);

                LOG.debug("loaded native library: {}", path);

                JPIPE_LOADED = true;
            } catch (UnsatisfiedLinkError linkex) {
                /*
                boolean app32Bit = null != path && path.toLowerCase().contains("program files (x86)");
                if (WINDOWS && JDK_64BIT && app32Bit) { }
                */
                UnsatisfiedLinkError error = new UnsatisfiedLinkError(
                        "Can't load jpipe native library (Current JDK is " + (JDK_64BIT ? "64-Bit" : "32-Bit") + "). \n" +
                                "\tPlease:\n" +
                                "\t\t1. Install OpenOffice/LibreOffice (http://zh-cn.libreoffice.org/download/libreoffice-still/)\n" +
                                "\t\t2. Configure OO_HOME in environment or oo.home in application\n" +
                                "\t\t3. Ensure native library is " + (JDK_64BIT ? "64-Bit" : "32-Bit") + "\n"
                );
                error.initCause(linkex);

                LOG.error("Can't load jpipe native library. ", error);

                throw error;
            }
        }
    }

    /**
     * 提供一个简化的 UnoUrl 语法解析, 默认使用 urp 协议, root id 为空
     * <p/>
     * [uno:]connection-type,params[;protocol-name,parameters;[objectname]]
     *
     * @param unoUrl UNO Url 字符串
     * @return UnoUrl 对象
     */
    public static UnoUrl parseUnoUrl(String unoUrl) {
        // syntax: [uno:]connection-type,parameters;protocol-name,parameters;objectname
        // simplify syntax: [uno:]connection-type,parameters[;protocol-name,parameters;[objectname]]
        if (null == unoUrl || "".equals(unoUrl.trim())) {
            throw new IllegalArgumentException("Uno Urls syntax error. simplify syntax: [uno:]connection-type,parameters[;protocol-name,parameters;[objectname]]");
        }

        int index = unoUrl.trim().indexOf(';');
        // 缺失 protocal, object 片段
        if (0 > index) {
            unoUrl += ";urp;";
        } else if (index == unoUrl.length() - 1) {
            unoUrl += "urp;";
        } else {
            index = unoUrl.indexOf(';', index + 1);
            // 缺失 object name 部分(object name 可为空)
            if (0 > index) {
                unoUrl += ";";
            }
        }

        try {
            UnoUrl url = UnoUrl.parseUnoUrl(unoUrl);
            String connType = url.getConnection();
            if (!"socket".equals(connType) && !"pipe".equals(connType)) {
                throw new IllegalArgumentException("illegal connection-type: " + connType);
            }
            return url;
        } catch (com.sun.star.lang.IllegalArgumentException ex) {
            String message = ex.getMessage();
            if (message.contains("syntax:")) {
                throw new IllegalArgumentException("Uno Urls syntax error. simplify syntax: [uno:]connection-type,parameters[;protocol-name,parameters;[objectname]]", ex);
            }
            throw new IllegalArgumentException(ex.getMessage());
        }
    }

    /**
     * 将给定的 URL 转换为符合规范的 url
     * <br>
     * 注意: 对于文件系统 URL 使用 file:// 开头, 而 java 是 file: 开头, 需要自行转换(eg: file:/opt -- file:///opt)
     *
     * @param url URL
     * @return 标准 url 字符串
     */
    public static String toUrl(URL url) {
        String protocol = url.getProtocol();
        if ("file".equals(protocol)) {
            return "file://" + url.getFile();
        }
        return url.toString();
    }

    public static String toUrl(File file) {
        try {
            return toUrl(file.toURI().toURL());
        } catch (MalformedURLException e) {
            String url = file.getAbsolutePath();
            url = url.startsWith("/") ? url : "/" + url;
            return "file://" + url;
        }
    }

    public static File toFile(URL url) {
        return UrlToFileMapper.mapUrlToFile(url);
    }

    public static String join(String sep, int... args) {
        if (null == args || args.length == 0) {
            return "";
        }
        StringBuilder buff = new StringBuilder();
        for (int i = 0; i < args.length; i++) {
            if (0 < i) {
                buff.append(sep);
            }
            buff.append(args[i]);
        }
        return buff.toString();
    }

    public static String join(String sep, String... args) {
        if (null == args || args.length == 0) {
            return "";
        }
        StringBuilder buff = new StringBuilder();
        for (int i = 0; i < args.length; i++) {
            if (0 < i) {
                buff.append(sep);
            }
            buff.append(args[i]);
        }
        return buff.toString();
    }

    public static String getFileExtension(String filename) {
        if (filename == null) {
            return null;
        }
        int pos = indexOfFileExtension(filename);
        return pos < 0 ? "" : filename.substring(pos + 1);
    }

    public static int indexOfFileExtension(String filename) {
        if (filename == null) {
            return -1;
        }
        filename = filename.replace('\\', '/');
        int sepPos = filename.lastIndexOf('/');
        int dotPos = filename.lastIndexOf('.');
        return dotPos > sepPos ? dotPos : -1;
    }

    private UnoUtils() {
    }

    static {
        // 获取 jurt.jar 版本
        String solarVersion;
        try {
            ProtectionDomain domain = PipeConnection.class.getProtectionDomain();
            URL url = domain.getCodeSource().getLocation();
            URLConnection conn = url.openConnection();
            Manifest manifest;
            if (conn instanceof JarURLConnection) {
                manifest = ((JarURLConnection) conn).getManifest();
            } else {
                JarInputStream jarIn = new JarInputStream(conn.getInputStream());
                manifest = jarIn.getManifest();
            }
            Attributes attrs = manifest.getMainAttributes();
            solarVersion = attrs.getValue("Solar-Version");
        } catch (Exception e) {
            solarVersion = "";
        }
        JURT_VERSION = solarVersion;
    }
}
